Skip to content

Conversation

@MeghP89
Copy link

@MeghP89 MeghP89 commented Nov 26, 2025

Summary by CodeRabbit

  • New Features
    • Event RSVP endpoints: submit and list RSVPs with auth, membership checks, rate limiting; endpoints exposed in development only.
  • Tests
    • Unit tests for RSVP POST and GET flows using mocked auth, membership checks, and storage.
  • Chores
    • New datastore provisioned for RSVPs, new config entry and role values, and a typed RSVP schema added.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Walkthrough

Adds RSVP functionality: new Fastify plugin exposing POST and GET RSVP endpoints (dev-registered), a new DynamoDB table with GSI and replicas, config and role additions, a Zod RSVP schema, and unit tests for POST and GET flows. (47 words)

Changes

Cohort / File(s) Summary
API route registration
src/api/index.ts
Imports and conditionally registers rsvpRoutes under /rsvp when run environment is dev.
RSVP routes
src/api/routes/rsvp.ts
New Fastify plugin exposing POST /:orgId/event/:eventId (verifies UIUC token, derives netId/upn, checks paid membership, creates RSVP item, PutItem to DynamoDB, returns 201) and GET /:orgId/event/:eventId (requires VIEW_RSVPS role, queries RSVPDynamoTableName via EventIdIndex, unmarshals, deduplicates by userId, returns list). Includes rate limiting, Zod/OpenAPI schemas, and auth hooks.
DynamoDB infra
terraform/modules/dynamo/main.tf
Adds events_rsvp DynamoDB table: PAY_PER_REQUEST, partitionKey hash key, eventId attribute, GSI EventIdIndex on eventId, streams NEW_AND_OLD_IMAGES, and dynamic replicas across ReplicationRegions.
Configuration
src/common/config.ts
Adds RSVPDynamoTableName to GenericConfigType and sets genericConfig.RSVPDynamoTableName = "infra-core-api-events-rsvp".
Roles
src/common/roles.ts
Adds RSVPS_MANAGER = "manage:rsvps" and VIEW_RSVPS = "view:rsvps" to BaseRoles and maps human-friendly names in AppRoleHumanMapper.
Types / Schema
src/common/types/rsvp.ts
New Zod schema rsvpItemSchema exporting fields: eventId, userId, isPaidMember, createdAt.
Tests
tests/unit/rsvps.test.ts
New unit tests mocking token verification, membership checks, and DynamoDB PutItem/Query; tests POST (201 + persisted item) and GET (returns deduplicated RSVP list).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as API (rsvpRoutes)
    participant Auth as Auth Module
    participant Membership as Membership Service
    participant DynamoDB

    rect rgb(235,245,255)
    Note over Client,API: POST /:orgId/event/:eventId
    Client->>API: POST with UIUC token (Authorization header)
    API->>Auth: verifyUiucAccessToken(token)
    Auth-->>API: principalName / upn
    API->>API: derive netId from upn
    API->>Membership: checkPaidMembership(netId, orgId)
    Membership->>DynamoDB: lookup membership (Dynamo/Redis)
    DynamoDB-->>Membership: membership flag
    Membership-->>API: isPaidMember
    API->>DynamoDB: PutItem into RSVPDynamoTableName
    DynamoDB-->>API: PutItem success
    API-->>Client: 201 Created (rsvp item)
    end

    rect rgb(250,250,230)
    Note over Client,API: GET /:orgId/event/:eventId
    Client->>API: GET (requires VIEW_RSVPS role)
    API->>DynamoDB: Query EventIdIndex for eventId
    DynamoDB-->>API: Items
    API->>API: Unmarshal, dedupe by userId
    API-->>Client: 200 OK (rsvp list)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

  • Areas to focus on:
    • src/api/routes/rsvp.ts: netId extraction/parsing, error handling, rate limiter placement, and correct use of AWS SDK commands.
    • terraform/modules/dynamo/main.tf: GSI key/attribute definitions, table naming, replication/stream settings.
    • src/common/config.ts and roles.ts: ensure new config/roles are consumed consistently.
    • tests/unit/rsvps.test.ts: mocks alignment with real SDK client signatures and auth/membership behavior.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the primary change: adding a GET endpoint to retrieve all RSVPs for an event, which aligns with the main additions (rsvp.ts route module, DynamoDB table, and supporting infrastructure).
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch events-rsvp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 26, 2025

💰 Infracost report

Monthly estimate increased by $6 📈

Changed project Baseline cost Usage cost* Total change New monthly cost
acm-uiuc/core/terraform/envs/prod +$0.00 +$3 +$3 (+4%) $86
acm-uiuc/core/terraform/envs/qa +$0.00 +$3 +$3 (+3%) $90

*Usage costs were estimated using infracost-usage.yml, see docs for other options.

Estimate details
Key: * usage cost, ~ changed, + added, - removed

──────────────────────────────────
Project: envs-prod
Module path: envs/prod

+ module.dynamo.aws_dynamodb_table.events_rsvp
  +$3

    + Write request unit (WRU)
      +$0.00, +150 WRUs*

    + Read request unit (RRU)
      +$0.00, +300 RRUs*

    + Data storage
      +$0.75, +3 GB*

    + Point-In-Time Recovery (PITR) backup storage
      +$1, +6 GB*

    + On-demand backup storage
      +$0.60, +6 GB*

    + Table data restored
      +$0.45, +3 GB*

    + Streams read request unit (sRRU)
      +$0.00, +700 sRRUs*

    + Global table (us-west-2)
    
        + Replicated write request unit (rWRU)
          +$0.00

Monthly cost change for acm-uiuc/core/terraform/envs/prod (Module path: envs/prod)
Amount:  +$3 ($83 → $86)
Percent: +4%

──────────────────────────────────
Project: envs-qa
Module path: envs/qa

+ module.dynamo.aws_dynamodb_table.events_rsvp
  +$3

    + Write request unit (WRU)
      +$0.00, +150 WRUs*

    + Read request unit (RRU)
      +$0.00, +300 RRUs*

    + Data storage
      +$0.75, +3 GB*

    + Point-In-Time Recovery (PITR) backup storage
      +$1, +6 GB*

    + On-demand backup storage
      +$0.60, +6 GB*

    + Table data restored
      +$0.45, +3 GB*

    + Streams read request unit (sRRU)
      +$0.00, +700 sRRUs*

    + Global table (us-west-2)
    
        + Replicated write request unit (rWRU)
          +$0.00

Monthly cost change for acm-uiuc/core/terraform/envs/qa (Module path: envs/qa)
Amount:  +$3 ($87 → $90)
Percent: +3%

──────────────────────────────────
Key: * usage cost, ~ changed, + added, - removed

*Usage costs were estimated using infracost-usage.yml, see docs for other options.

340 cloud resources were detected:
∙ 134 were estimated
∙ 206 were free

Infracost estimate: Monthly estimate increased by $6 ↑
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Changed project                                    ┃ Baseline cost ┃ Usage cost* ┃ Total change ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━╋━━━━━━━━━━━━━━┫
┃ acm-uiuc/core/terraform/envs/prod                  ┃        +$0.00 ┃         +$3 ┃    +$3 (+4%) ┃
┃ acm-uiuc/core/terraform/envs/qa                    ┃        +$0.00 ┃         +$3 ┃    +$3 (+3%) ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛
This comment will be updated when code changes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
src/api/routes/rsvp.ts (1)

3-3: Unused imports: withRoles and getUserOrgRoles.

These imports are not currently used. If authorization is added per the previous comment, they will be needed. Otherwise, remove them.

Also applies to: 6-6

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae6f87 and 9e2f331.

📒 Files selected for processing (3)
  • src/api/index.ts (2 hunks)
  • src/api/routes/rsvp.ts (1 hunks)
  • terraform/modules/dynamo/main.tf (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/api/routes/rsvp.ts (5)
src/api/components/index.ts (1)
  • withTags (243-258)
src/api/functions/uin.ts (1)
  • verifyUiucAccessToken (30-96)
src/common/errors/index.ts (1)
  • ValidationError (99-108)
src/api/functions/membership.ts (1)
  • checkPaidMembership (359-400)
src/common/config.ts (1)
  • genericConfig (244-244)
🪛 ESLint
src/api/routes/rsvp.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/pMkMEsaHQW'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 2-2: Unexpected use of file extension "js" for "api/plugins/rateLimiter.js"

(import/extensions)


[error] 3-3: Unexpected use of file extension "js" for "api/components/index.js"

(import/extensions)


[error] 6-6: Unexpected use of file extension "js" for "api/functions/organizations.js"

(import/extensions)


[error] 11-11: Unexpected use of file extension "js" for "common/errors/index.js"

(import/extensions)


[error] 13-13: Unexpected use of file extension "js" for "api/functions/uin.js"

(import/extensions)


[error] 14-14: Unexpected use of file extension "js" for "api/functions/membership.js"

(import/extensions)


[error] 16-16: Unexpected use of file extension "js" for "common/config.js"

(import/extensions)

src/api/index.ts

[error] 65-65: Unexpected use of file extension "js" for "./routes/rsvp.js"

(import/extensions)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build Application
🔇 Additional comments (1)
src/api/index.ts (1)

65-65: LGTM!

The import and conditional registration of rsvpRoutes follows the existing patterns in the codebase. The dev-only gate is consistent with how vendingPlugin is registered.

Also applies to: 381-383

Comment on lines +43 to +46
"x-uiuc-token": z.jwt().min(1).meta({
description:
"An access token for the user in the UIUC Entra ID tenant.",
}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Script executed:

#!/bin/bash
# Search for z.jwt() usage and any custom JWT schema definitions
rg -n "z\.jwt|\.jwt\(\)" --type ts -C 3

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/api/routes/syncIdentity.ts-64-      {
src/api/routes/syncIdentity.ts-65-        schema: withTags(["Generic"], {
src/api/routes/syncIdentity.ts-66-          headers: z.object({
src/api/routes/syncIdentity.ts:67:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/syncIdentity.ts-68-              description:
src/api/routes/syncIdentity.ts-69-                "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/syncIdentity.ts-70-            }),
--
src/api/routes/syncIdentity.ts-150-      {
src/api/routes/syncIdentity.ts-151-        schema: withTags(["Generic"], {
src/api/routes/syncIdentity.ts-152-          headers: z.object({
src/api/routes/syncIdentity.ts:153:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/syncIdentity.ts-154-              description:
src/api/routes/syncIdentity.ts-155-                "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/syncIdentity.ts-156-            }),
--
src/api/routes/rsvp.ts-40-          }),
src/api/routes/rsvp.ts-41-        }),
src/api/routes/rsvp.ts-42-        headers: z.object({
src/api/routes/rsvp.ts:43:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/rsvp.ts-44-            description:
src/api/routes/rsvp.ts-45-              "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/rsvp.ts-46-          }),
--
src/api/routes/rsvp.ts-92-          }),
src/api/routes/rsvp.ts-93-        }),
src/api/routes/rsvp.ts-94-        headers: z.object({
src/api/routes/rsvp.ts:95:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/rsvp.ts-96-            description:
src/api/routes/rsvp.ts-97-              "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/rsvp.ts-98-          }),
--
src/api/routes/v2/mobileWallet.ts-27-      schema: withTags(["Mobile Wallet"], {
src/api/routes/v2/mobileWallet.ts-28-        summary: "Retrieve mobile wallet pass for ACM member.",
src/api/routes/v2/mobileWallet.ts-29-        headers: z.object({
src/api/routes/v2/mobileWallet.ts:30:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/v2/mobileWallet.ts-31-            description:
src/api/routes/v2/mobileWallet.ts-32-              "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/v2/mobileWallet.ts-33-          }),
--
src/api/routes/v2/membership.ts-77-      {
src/api/routes/v2/membership.ts-78-        schema: withTags(["Membership"], {
src/api/routes/v2/membership.ts-79-          headers: z.object({
src/api/routes/v2/membership.ts:80:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/v2/membership.ts-81-              description:
src/api/routes/v2/membership.ts-82-                "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/v2/membership.ts-83-            }),
--
src/api/routes/membership.ts-45-      {
src/api/routes/membership.ts-46-        schema: withTags(["Membership"], {
src/api/routes/membership.ts-47-          headers: z.object({
src/api/routes/membership.ts:48:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/membership.ts-49-              description:
src/api/routes/membership.ts-50-                "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/membership.ts-51-            }),

</shell_result>


Script executed:

#!/bin/bash
# Check for Zod version in package.json and any custom extensions
cat package.json | grep -A2 -B2 "zod"

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Read the specific file to see context around lines 43-46 and 95-98
head -100 src/api/routes/rsvp.ts | tail -60

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

        }),
        headers: z.object({
          "x-uiuc-token": z.jwt().min(1).meta({
            description:
              "An access token for the user in the UIUC Entra ID tenant.",
          }),
        }),
      }),
    },
    async (request, reply) => {
      const accessToken = request.headers["x-uiuc-token"];
      const verifiedData = await verifyUiucAccessToken({
        accessToken,
        logger: request.log,
      });
      const { userPrincipalName: upn, givenName, surname } = verifiedData;
      const netId = upn.replace("@illinois.edu", "");
      if (netId.includes("@")) {
        request.log.error(
          `Found UPN ${upn} which cannot be turned into NetID via simple replacement.`,
        );
        throw new ValidationError({
          message: "ID token could not be parsed.",
        });
      }
      const isPaidMember = await checkPaidMembership({
        netId,
        dynamoClient: fastify.dynamoClient,
        redisClient: fastify.redisClient,
        logger: request.log,
      });
      const entry = {
        partitionKey: `${request.params.eventId}#${upn}`,
        eventId: request.params.eventId,
        userId: upn,
        isPaidMember,
        createdAt: "",
      };
    },
  );
  fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
    "/:orgId/event/:eventId",
    {
      schema: withTags(["RSVP"], {
        summary: "Get all RSVPs for an event.",
        params: z.object({
          eventId: z.string().min(1).meta({
            description: "The previously-created event ID in the events API.",
          }),
          orgId: z.string().min(1).meta({
            description: "The organization ID the event belongs to.",
          }),
        }),
        headers: z.object({
          "x-uiuc-token": z.jwt().min(1).meta({
            description:
              "An access token for the user in the UIUC Entra ID tenant.",
          }),
        }),
      }),

</shell_result>


Script executed:

#!/bin/bash
# Search for any zod extensions or custom schema definitions
rg -n "z\.|extend\(|.jwt" src/api/routes/rsvp.ts -C 2

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

16-import { genericConfig } from "common/config.js";
17-
18:const rsvpItemSchema = z.object({
19:  eventId: z.string(),
20:  userId: z.string(),
21:  isPaidMember: z.boolean(),
22:  createdAt: z.string(),
23-});
24:const rsvpListSchema = z.array(rsvpItemSchema);
25-
26-const rsvpRoutes: FastifyPluginAsync = async (fastify, _options) => {
--
35-      schema: withTags(["RSVP"], {
36-        summary: "Submit an RSVP for an event.",
37:        params: z.object({
38:          eventId: z.string().min(1).meta({
39-            description: "The previously-created event ID in the events API.",
40-          }),
41-        }),
42:        headers: z.object({
43:          "x-uiuc-token": z.jwt().min(1).meta({
44-            description:
45-              "An access token for the user in the UIUC Entra ID tenant.",
--
84-      schema: withTags(["RSVP"], {
85-        summary: "Get all RSVPs for an event.",
86:        params: z.object({
87:          eventId: z.string().min(1).meta({
88-            description: "The previously-created event ID in the events API.",
89-          }),
90:          orgId: z.string().min(1).meta({
91-            description: "The organization ID the event belongs to.",
92-          }),
93-        }),
94:        headers: z.object({
95:          "x-uiuc-token": z.jwt().min(1).meta({
96-            description:
97-              "An access token for the user in the UIUC Entra ID tenant.",
--
111-      const response = await fastify.dynamoClient.send(commnand);
112-      const items = response.Items?.map((item) => unmarshall(item)) || [];
113:      return reply.send(items as z.infer<typeof rsvpListSchema>);
114-    },
115-  );

</shell_result>


Script executed:

#!/bin/bash
# Search for jwt function definition or Zod extension
rg -n "jwt\s*[:=]|\.jwt\s*=|extend.*jwt" --type ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Check imports in rsvp.ts to understand where z comes from
head -20 src/api/routes/rsvp.ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

import { FastifyPluginAsync } from "fastify";
import rateLimiter from "api/plugins/rateLimiter.js";
import { withRoles, withTags } from "api/components/index.js";
import { QueryCommand } from "@aws-sdk/client-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";
import { getUserOrgRoles } from "api/functions/organizations.js";
import {
  UnauthenticatedError,
  UnauthorizedError,
  ValidationError,
} from "common/errors/index.js";
import * as z from "zod/v4";
import { verifyUiucAccessToken } from "api/functions/uin.js";
import { checkPaidMembership } from "api/functions/membership.js";
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
import { genericConfig } from "common/config.js";

const rsvpItemSchema = z.object({
  eventId: z.string(),
  userId: z.string(),

</shell_result>


Script executed:

#!/bin/bash
# Look for zod configuration or extensions files
fd -e ts -e js | xargs rg -l "z\.extend|ZodType.*jwt" | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

</shell_result>


Script executed:

#!/bin/bash
# Check package.json for zod version and look at dependencies
cat package.json | head -50

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

{
  "name": "infra-core",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "workspaces": [
    "src/api",
    "src/ui",
    "src/archival",
    "src/linkryEdgeFunction"
  ],
  "packageManager": "yarn@1.22.22",
  "scripts": {
    "postinstall": "npm run setup",
    "setup": "git config blame.ignoreRevsFile .git-blame-ignore-revs",
    "build": "concurrently --names 'api,ui,archival,linkryEdge' 'yarn workspace infra-core-api run build' 'yarn workspace infra-core-ui run build' 'yarn workspace infra-core-archival run build' 'yarn workspace infra-core-linkry-edge run build'",
    "postbuild": "node src/api/createLambdaPackage.js && yarn lockfile-manage",
    "dev": "cross-env DISABLE_AUDIT_LOG=true concurrently --names 'api,ui' 'yarn workspace infra-core-api run dev' 'yarn workspace infra-core-ui run dev'",
    "lockfile-manage": "cp src/api/package.lambda.json dist/lambda/package.json && cp src/api/package.lambda.json dist/sqsConsumer/package.json && cp yarn.lock dist/lambda/ && cp yarn.lock dist/sqsConsumer/",
    "prettier": "yarn workspaces run prettier && prettier --check tests/**/*.ts",
    "prettier:write": "yarn workspaces run prettier:write && prettier --write tests/**/*.ts",
    "lint": "yarn workspaces run lint",
    "prepare": "node .husky/install.mjs || true",
    "typecheck": "yarn workspaces run typecheck",
    "test:unit": "cross-env RunEnvironment='dev' concurrently --names 'api,ui' 'yarn run test:unit-api' 'yarn workspace infra-core-ui run test:unit'",
    "test:unit-api": "cross-env RunEnvironment='dev' vitest run --coverage --config tests/unit/vitest.config.ts tests/unit",
    "test:unit-ui": "yarn test:unit --ui",
    "test:unit-watch": "vitest tests/unit",
    "test:live": "vitest tests/live",
    "test:live-ui": "yarn test:live --ui",
    "test:e2e": "playwright test",
    "test:e2e-ui": "playwright test --ui"
  },
  "dependencies": {
    "@acm-uiuc/js-shared": "^3.2.1"
  },
  "devDependencies": {
    "@eslint/compat": "^1.3.2",
    "@eslint/eslintrc": "^3.3.1",
    "@eslint/js": "^9.33.0",
    "@playwright/test": "^1.56.1",
    "@smithy/types": "^4.5.0",
    "@tsconfig/node22": "^22.0.1",
    "@types/ioredis-mock": "^8.2.5",
    "@types/node": "^24.3.0",
    "@types/pluralize": "^0.0.33",
    "@types/react": "^19.2.2",
    "@types/react-dom": "^19.2.2",
    "@types/supertest": "^6.0.3",
    "@typescript-eslint/eslint-plugin": "^8.40.0",

</shell_result>


Script executed:

#!/bin/bash
# Search for any z.jwt or jwt extension setup
rg -n "jwt" --type ts -g "!node_modules" | grep -i "extend\|z\.jwt\|define" | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/api/routes/rsvp.ts:43:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/rsvp.ts:95:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/v2/membership.ts:80:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/v2/mobileWallet.ts:30:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/syncIdentity.ts:67:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/syncIdentity.ts:153:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/membership.ts:48:            "x-uiuc-token": z.jwt().min(1).meta({

</shell_result>


Script executed:

#!/bin/bash
# Look for zod setup or configuration files
fd -type f \( -name "*zod*" -o -name "*config*" \) --type ts --type js | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stderr:

error: 'ype' isn't a valid value for '--type <filetype>'
  [possible values: file, directory, symlink, executable, empty, socket, pipe]

For more information try '--help'

</shell_result>


Script executed:

#!/bin/bash
# Search in common directory for any Zod extensions
rg -n "z\.|jwt" src/common --type ts -C 2 | head -50

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/common/utils.ts-38-
src/common/utils.ts-39-
src/common/utils.ts:40:export const nonEmptyCommaSeparatedStringSchema = z.
src/common/utils.ts:41:  array(z.string().min(1)).
src/common/utils.ts-42-  min(1, { message: "Filter expression must select at least one item." }).
src/common/utils.ts-43-  transform((val) => val.map((item) => item.trim()))
--
src/common/utils.ts-48-export const getDefaultFilteringQuerystring = ({ defaultSelect }: GetDefaultFilteringQuerystringInput) => {
src/common/utils.ts-49-  return {
src/common/utils.ts:50:    select: z.optional(nonEmptyCommaSeparatedStringSchema).default(defaultSelect).meta({
src/common/utils.ts-51-      description: "A list of attributes to return.",
src/common/utils.ts-52-      ...(defaultSelect.length === 0 ? { default: ["<ALL ATTRIBUTES>"] } : { example: defaultSelect })
--
src/common/types/user.ts-2-import { illinoisUin } from "./generic.js";
src/common/types/user.ts-3-
src/common/types/user.ts:4:export const searchUserByUinRequest = z.object({
src/common/types/user.ts-5-  uin: illinoisUin
src/common/types/user.ts-6-});
src/common/types/user.ts-7-
src/common/types/user.ts:8:export const searchUserByUinResponse = z.object({
src/common/types/user.ts:9:  email: z.email(),
src/common/types/user.ts-10-});
--
src/common/types/tickets.ts-1-import * as z from "zod/v4";
src/common/types/tickets.ts:2:export const postMetadataSchema = z.object({
src/common/types/tickets.ts:3:  type: z.union([z.literal("merch"), z.literal("ticket")]),
src/common/types/tickets.ts:4:  itemSalesActive: z.union([z.date(), z.boolean()])
src/common/types/tickets.ts-5-});
src/common/types/tickets.ts-6-
src/common/types/tickets.ts:7:export type ItemPostData = z.infer<typeof postMetadataSchema>;
--
src/common/types/stripe.ts-1-import * as z from "zod/v4";
src/common/types/stripe.ts-2-
src/common/types/stripe.ts:3:export const invoiceLinkPostResponseSchema = z.object({
src/common/types/stripe.ts:4:  id: z.string().min(1),
src/common/types/stripe.ts:5:  link: z.string().url()
src/common/types/stripe.ts-6-});
src/common/types/stripe.ts-7-
src/common/types/stripe.ts:8:export const invoiceLinkPostRequestSchema = z.object({
src/common/types/stripe.ts:9:  invoiceId: z.string().min(1),
src/common/types/stripe.ts:10:  invoiceAmountUsd: z.number().min(50),
src/common/types/stripe.ts:11:  contactName: z.string().min(1),
src/common/types/stripe.ts:12:  contactEmail: z.string().email(),
src/common/types/stripe.ts:13:  achPaymentsEnabled: z.optional(z.boolean()).default(false),
src/common/types/stripe.ts-14-});
src/common/types/stripe.ts-15-
src/common/types/stripe.ts:16:export type PostInvoiceLinkRequest = z.infer<
src/common/types/stripe.ts-17-  typeof invoiceLinkPostRequestSchema>;
src/common/types/stripe.ts-18-
src/common/types/stripe.ts-19-

</shell_result>


Script executed:

#!/bin/bash
# Check API workspace for zod version
cat src/api/package.json | grep -A 2 -B 2 "zod"

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

    "fastify-plugin": "^5.0.1",
    "fastify-raw-body": "^5.0.0",
    "fastify-zod-openapi": "^5.0.1",
    "handlebars": "^4.7.8",
    "ical-generator": "^10.0.0",
--
    "stripe": "^19.2.0",
    "uuid": "^13.0.0",
    "zod": "^4.0.14",
    "zod-validation-error": "^4.0.1"
  },
  "devDependencies": {

</shell_result>


Script executed:

#!/bin/bash
# Search for where jwt() might be extended onto z
rg -n "\.jwt\s*=" --type ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Look for any custom Zod types or extensions
rg -n "extend\|\.jwt\|jwt\(" --type ts | grep -v "x-uiuc-token" | head -30

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

</shell_result>


Script executed:

#!/bin/bash
# Check if zod has jwt method in v4
web_search "Zod v4 jwt method"

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 127

stderr:

/bin/bash: line 3: web_search: command not found

</shell_result>


Script executed:

#!/bin/bash
# Look for .d.ts files or type augmentation
find . -name "*.d.ts" -o -name "*types*" | grep -v node_modules | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

./src/api/resources/types.d.ts
./src/api/types.d.ts
./src/common/types
./src/ui/types.d.ts
./src/ui/vite-env.d.ts

</shell_result>


Script executed:

#!/bin/bash
# Search for any Zod augmentation or type declaration
rg -n "declare|augment|namespace.*z|interface.*z" --type ts | grep -i zod | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

</shell_result>


Script executed:

#!/bin/bash
# Check if there's a tsconfig that might reveal anything
cat tsconfig.json | head -50

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stderr:

cat: tsconfig.json: No such file or directory

</shell_result>


Script executed:

#!/bin/bash
# More thorough search for "jwt" across all files
rg -n "jwt" --type ts -C 1 | head -50

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

tests/unit/tickets.test.ts-31-const ddbMock = mockClient(DynamoDBClient);
tests/unit/tickets.test.ts:32:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/tickets.test.ts:33:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/tickets.test.ts-34-
--
tests/unit/secret.testdata.ts-18-const testSecretObject = {
tests/unit/secret.testdata.ts:19:  jwt_key: "somethingreallysecret",
tests/unit/secret.testdata.ts-20-};
--
tests/unit/secret.testdata.ts-28-
tests/unit/secret.testdata.ts:29:const jwtPayload = {
tests/unit/secret.testdata.ts:30:  aud: "custom_jwt",
tests/unit/secret.testdata.ts:31:  iss: "custom_jwt",
tests/unit/secret.testdata.ts-32-  iat: Math.floor(Date.now() / 1000),
--
tests/unit/secret.testdata.ts-54-
tests/unit/secret.testdata.ts:55:const jwtPayloadNoGroups = {
tests/unit/secret.testdata.ts:56:  aud: "custom_jwt",
tests/unit/secret.testdata.ts:57:  iss: "custom_jwt",
tests/unit/secret.testdata.ts-58-  iat: Math.floor(Date.now() / 1000),
--
tests/unit/secret.testdata.ts-85-  testSecretObject,
tests/unit/secret.testdata.ts:86:  jwtPayload,
tests/unit/secret.testdata.ts:87:  jwtPayloadNoGroups,
tests/unit/secret.testdata.ts-88-  uinSecretJson,
--
tests/unit/logs.test.ts-13-const ddbMock = mockClient(DynamoDBClient);
tests/unit/logs.test.ts:14:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/logs.test.ts:15:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/logs.test.ts-16-
--
tests/unit/linkry.test.ts-17-const ddbMock = mockClient(DynamoDBClient);
tests/unit/linkry.test.ts:18:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/linkry.test.ts:19:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/linkry.test.ts-20-
--
tests/unit/ical.test.ts-12-const ddbMock = mockClient(DynamoDBClient);
tests/unit/ical.test.ts:13:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/ical.test.ts:14:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/ical.test.ts-15-
--
tests/unit/events.test.ts-15-const ddbMock = mockClient(DynamoDBClient);
tests/unit/events.test.ts:16:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/events.test.ts:17:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/events.test.ts-18-
--
tests/unit/eventPost.test.ts-19-const ddbMock = mockClient(DynamoDBClient);
tests/unit/eventPost.test.ts:20:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/eventPost.test.ts:21:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/eventPost.test.ts-22-

</shell_result>


Script executed:

#!/bin/bash
# Read the API types file which likely contains the jwt() extension
cat src/api/types.d.ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

/* eslint-disable @typescript-eslint/no-unused-vars */
import { FastifyRequest, FastifyInstance, FastifyReply } from "fastify";
import { AppRoles, RunEnvironment } from "../common/roles.js";
import { AadToken } from "./plugins/auth.js";
import { ConfigType, SecretConfig, SecretTesting } from "../common/config.js";
import NodeCache from "node-cache";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { SQSClient } from "@aws-sdk/client-sqs";
import { AvailableAuthorizationPolicy } from "common/policies/definition.js";
import type RedisModule from "ioredis";
import { type S3Client } from "@aws-sdk/client-s3";
export type Redis = RedisModule.default;
export type ValidLoggers = FastifyBaseLogger | pino.Logger;

interface CloudfrontLocation {
  country: string | undefined;
  city: string | undefined;
  region: string | undefined;
  latitude: string | undefined;
  longitude: string | undefined;
  postalCode: string | undefined;
}

declare module "fastify" {
  interface FastifyInstance {
    authenticate: (
      request: FastifyRequest,
      reply: FastifyReply,
    ) => Promise<void>;
    authorize: (
      request: FastifyRequest,
      reply: FastifyReply,
      validRoles: AppRoles[],
      disableApiKeyAuth: boolean,
    ) => Promise<Set<AppRoles>>;
    authorizeFromSchema: (
      request: FastifyRequest,
      reply: FastifyReply,
    ) => Promise<void>;
    runEnvironment: RunEnvironment;
    environmentConfig: ConfigType;
    nodeCache: NodeCache;
    dynamoClient: DynamoDBClient;
    sqsClient?: SQSClient;
    s3Client?: S3Client;
    redisClient: Redis;
    secretsManagerClient: SecretsManagerClient;
    secretConfig: SecretConfig | (SecretConfig & SecretTesting);
    refreshSecretConfig: CallableFunction;
  }
  interface FastifyRequest {
    startTime: number;
    username?: string;
    userRoles?: Set<AppRoles>;
    tokenPayload?: AadToken;
    policyRestrictions?: AvailableAuthorizationPolicy[];
    location: CloudfrontLocation;
  }
}

export type NoDataRequest = {
  Params: undefined;
  Querystring: undefined;
  Body: undefined;
};

</shell_result>


Script executed:

#!/bin/bash
# Also check the resources types
cat src/api/resources/types.d.ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

/* eslint-disable @typescript-eslint/no-explicit-any */

declare module "*.png" {
  const value: string;
  export default value;
}

declare module "*.json" {
  const value: Record<string, any>;
  export default value;
}

</shell_result>


Script executed:

#!/bin/bash
# Search for fastify-zod-openapi imports and usage
rg -n "fastify-zod-openapi" --type ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/api/components/index.ts:2:import { FastifyZodOpenApiSchema } from "fastify-zod-openapi";
src/api/index.ts:24:} from "fastify-zod-openapi";
src/api/routes/ics.ts:24:} from "fastify-zod-openapi";
src/api/routes/logs.ts:12:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/membership.ts:23:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/organizations.ts:12:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/mobileWallet.ts:15:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/rsvp.ts:15:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/roomRequests.ts:29:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/vending.ts:3:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/user.ts:16:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/syncIdentity.ts:8:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/tickets.ts:28:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/stripe.ts:42:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/iam.ts:46:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/linkry.ts:47:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/v2/mobileWallet.ts:12:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/v2/membership.ts:18:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/apiKey.ts:5:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/plugins/authorizeFromSchema.ts:4:import { FastifyZodOpenApiSchema } from "fastify-zod-openapi";
src/api/routes/events.ts:44:} from "fastify-zod-openapi";

</shell_result>


Script executed:

#!/bin/bash
# Check if there's any setup file that might extend Zod
rg -n "import.*z|from.*zod" src/api --type ts | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/api/index.ts:24:} from "fastify-zod-openapi";
src/api/index.ts:25:import { type ZodOpenApiVersion } from "zod-openapi";
src/api/index.ts:39:import authorizeFromSchemaPlugin from "./plugins/authorizeFromSchema.js";
src/api/index.ts:44:import organizationsPlugin from "./routes/organizations.js";
src/api/types.d.ts:10:import { AvailableAuthorizationPolicy } from "common/policies/definition.js";
src/api/routes/mobileWallet.ts:6:import * as z from "zod/v4";
src/api/routes/mobileWallet.ts:15:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/logs.ts:12:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/logs.ts:13:import * as z from "zod/v4";
src/api/routes/tickets.ts:2:import * as z from "zod/v4";
src/api/routes/tickets.ts:28:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/membership.ts:23:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/membership.ts:24:import * as z from "zod/v4";
src/api/routes/vending.ts:3:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/vending.ts:4:import * as z from "zod/v4";
src/api/routes/rsvp.ts:6:import { getUserOrgRoles } from "api/functions/organizations.js";
src/api/routes/rsvp.ts:12:import * as z from "zod/v4";
src/api/routes/rsvp.ts:15:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/user.ts:4:import { getUserOrgRoles } from "api/functions/organizations.js";
src/api/routes/user.ts:10:import * as z from "zod/v4";

</shell_result>


Zod v4 jwt method


<web_search_result>
Short answer: Zod v4 adds a string validator for JWTs: use z.string().jwt() (optionally z.string().jwt({ alg: "RS256" }) to require a specific alg). It only checks the three-part/base64url structure (and decodes header/payload), it does NOT verify signatures — use a JWT library (e.g., jsonwebtoken or jose) for cryptographic verification. [1][2]
</web_search_result>


Fix z.jwt() to z.string().jwt()z.jwt() is not a valid Zod v4 method.

Zod v4 provides z.string().jwt() to validate JWT strings, not z.jwt(). The current code will fail at runtime. Replace z.jwt().min(1) with z.string().jwt().min(1).

Note: z.string().jwt() validates the JWT structure (three-part base64url format) but does not verify signatures—use a JWT library like jose for cryptographic verification.

🤖 Prompt for AI Agents
In src/api/routes/rsvp.ts around lines 43-46, replace the invalid Zod call
z.jwt().min(1) with z.string().jwt().min(1) so the header schema uses
z.string().jwt().min(1) to validate JWT-formatted strings; ensure imports still
reference z from zod and keep min(1) in the chain (or move before jwt() if
preferred).

Comment on lines 72 to 79
const entry = {
partitionKey: `${request.params.eventId}#${upn}`,
eventId: request.params.eventId,
userId: upn,
isPaidMember,
createdAt: "",
};
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

POST handler is incomplete — entry is created but never persisted or returned.

The handler constructs an RSVP entry object but:

  1. Never writes it to DynamoDB
  2. Never sends a response to the client
  3. Sets createdAt to an empty string instead of a timestamp

The handler needs to persist the entry and return a response. Example fix:

       const entry = {
         partitionKey: `${request.params.eventId}#${upn}`,
         eventId: request.params.eventId,
         userId: upn,
         isPaidMember,
-        createdAt: "",
+        createdAt: new Date().toISOString(),
       };
+      const putCommand = new PutItemCommand({
+        TableName: genericConfig.RsvpDynamoTableName, // Add this config
+        Item: marshall(entry),
+      });
+      await fastify.dynamoClient.send(putCommand);
+      return reply.status(201).send(entry);
     },

You'll also need to import PutItemCommand from @aws-sdk/client-dynamodb and marshall from @aws-sdk/util-dynamodb.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/api/routes/rsvp.ts around lines 72-79 the POST handler builds an RSVP
entry but never persists it or responds to the client and createdAt is empty;
fix by importing PutItemCommand from @aws-sdk/client-dynamodb and marshall from
@aws-sdk/util-dynamodb, set entry.createdAt = new Date().toISOString(), create a
PutItemCommand with TableName and Item: marshall(entry), await
dynamoDbClient.send(putCmd) (wrap in try/catch to handle errors), and finally
return an appropriate HTTP response (e.g., 201 with the saved entry or a
location header) or a 500 on failure.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/api/routes/rsvp.ts (2)

38-43: Fix z.jwt() to z.string().jwt().

Zod v4 provides z.string().jwt() for JWT validation, not z.jwt(). This will cause a runtime error.

         headers: z.object({
-          "x-uiuc-token": z.jwt().min(1).meta({
+          "x-uiuc-token": z.string().jwt().min(1).meta({
             description:
               "An access token for the user in the UIUC Entra ID tenant.",
           }),
         }),

68-74: Set createdAt to actual timestamp.

createdAt is set to an empty string, which loses the timestamp information. Use new Date().toISOString() for a proper ISO 8601 timestamp.

       const entry = {
         partitionKey: `${request.params.eventId}#${upn}`,
         eventId: request.params.eventId,
         userId: upn,
         isPaidMember,
-        createdAt: "",
+        createdAt: new Date().toISOString(),
       };
🧹 Nitpick comments (2)
tests/unit/rsvps.test.ts (1)

119-156: GET test looks good, but consider adding edge case coverage.

The happy path is well tested. Consider adding tests for:

  • Empty results (no RSVPs for event)
  • Authorization failures (missing VIEW_RSVPS role)
  • Invalid event ID
src/api/routes/rsvp.ts (1)

52-52: Unused destructured variables.

givenName and surname are destructured but never used. Consider removing them or using them (e.g., for logging).

-      const { userPrincipalName: upn, givenName, surname } = verifiedData;
+      const { userPrincipalName: upn } = verifiedData;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 9e2f331 and 9e6ba13.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (6)
  • src/api/routes/rsvp.ts (1 hunks)
  • src/common/config.ts (2 hunks)
  • src/common/roles.ts (1 hunks)
  • src/common/types/rsvp.ts (1 hunks)
  • terraform/modules/dynamo/main.tf (1 hunks)
  • tests/unit/rsvps.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • terraform/modules/dynamo/main.tf
🧰 Additional context used
🧬 Code graph analysis (1)
src/api/routes/rsvp.ts (5)
src/api/components/index.ts (2)
  • withTags (243-258)
  • withRoles (194-241)
src/api/functions/uin.ts (1)
  • verifyUiucAccessToken (30-96)
src/common/errors/index.ts (2)
  • ValidationError (99-108)
  • DatabaseFetchError (121-130)
src/api/functions/membership.ts (1)
  • checkPaidMembership (359-400)
src/common/roles.ts (2)
  • AppRoles (35-35)
  • AppRoles (36-36)
🪛 ESLint
tests/unit/rsvps.test.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/XtUyUAvhlL'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 9-9: Unexpected use of file extension "js" for "../../src/api/index.js"

(import/extensions)


[error] 10-10: Unexpected use of file extension "js" for "./auth.test.js"

(import/extensions)


[error] 11-11: Unexpected use of file extension "js" for "./secret.testdata.js"

(import/extensions)


[error] 12-12: Unexpected use of file extension "js" for "../../src/api/types.js"

(import/extensions)


[error] 74-74: ["jwt_key"] is better written in dot notation.

(dot-notation)

src/common/types/rsvp.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/txSPdyxJiv'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)

src/api/routes/rsvp.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/MgMsEepBoK'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 2-2: Unexpected use of file extension "js" for "api/plugins/rateLimiter.js"

(import/extensions)


[error] 3-3: Unexpected use of file extension "js" for "api/components/index.js"

(import/extensions)


[error] 11-11: Unexpected use of file extension "js" for "common/errors/index.js"

(import/extensions)


[error] 13-13: Unexpected use of file extension "js" for "api/functions/uin.js"

(import/extensions)


[error] 14-14: Unexpected use of file extension "js" for "api/functions/membership.js"

(import/extensions)


[error] 16-16: Unexpected use of file extension "js" for "common/config.js"

(import/extensions)


[error] 17-17: Unexpected use of file extension "js" for "common/roles.js"

(import/extensions)


[error] 119-119: Insert ,

(prettier/prettier)

🪛 GitHub Actions: QA deploy - @MeghP89
src/api/routes/rsvp.ts

[error] 119-119: prettier/prettier: Insert ,

🪛 GitHub Check: Run Unit Tests
src/api/routes/rsvp.ts

[failure] 119-119:
Insert ,

🪛 Gitleaks (8.29.1)
tests/unit/rsvps.test.ts

[high] 16-16: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🔇 Additional comments (4)
src/common/config.ts (1)

41-41: LGTM!

The RSVPDynamoTableName configuration is properly added to both the type definition and the config object, following the existing naming conventions.

Also applies to: 88-88

tests/unit/rsvps.test.ts (2)

15-16: Test JWT constant is fine — Gitleaks flag is a false positive.

This is a well-known example JWT from jwt.io documentation used purely for testing. It contains no sensitive data and is safe to use in test fixtures.


85-117: POST test looks good.

The test correctly verifies the RSVP creation flow, including the response structure and DynamoDB interaction.

src/common/types/rsvp.ts (1)

1-8: I'll verify the concerns in this review comment by checking if the schema is actually unused and whether there's duplication in the codebase.
<function_calls>

shell
#!/bin/bash

Check if rsvpItemSchema from common/types/rsvp is imported anywhere

echo "=== Checking for imports from common/types/rsvp ==="
rg -n "from.*common/types/rsvp" --type ts

echo ""
echo "=== Checking for all rsvpItemSchema definitions ==="
rg -n "rsvpItemSchema" --type ts -B2 -A2

echo ""
echo "=== Checking for rsvpListSchema ==="
rg -n "rsvpListSchema" --type ts -B2 -A2


</function_calls>

Comment on lines +112 to +116
if (!response || !response.Items) {
throw new DatabaseFetchError({
message: "Failed to get all member lists.",
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix misleading error message.

The error message says "Failed to get all member lists" but this endpoint retrieves RSVPs.

       if (!response || !response.Items) {
         throw new DatabaseFetchError({
-          message: "Failed to get all member lists.",
+          message: "Failed to fetch RSVPs for event.",
         });
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!response || !response.Items) {
throw new DatabaseFetchError({
message: "Failed to get all member lists.",
});
}
if (!response || !response.Items) {
throw new DatabaseFetchError({
message: "Failed to fetch RSVPs for event.",
});
}
🤖 Prompt for AI Agents
In src/api/routes/rsvp.ts around lines 112 to 116, the thrown DatabaseFetchError
uses a misleading message "Failed to get all member lists" even though this
endpoint retrieves RSVPs; update the error message to accurately reflect the
operation (e.g., "Failed to get all RSVPs" or "Failed to fetch RSVPs") while
keeping the same error type and payload structure so callers and logs report the
correct failure context.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/api/routes/rsvp.ts (3)

38-43: Fix z.jwt() to z.string().jwt().

z.jwt() is not a valid Zod v4 method. This will cause a runtime error. Use z.string().jwt() instead.

         headers: z.object({
-          "x-uiuc-token": z.jwt().min(1).meta({
+          "x-uiuc-token": z.string().jwt().min(1).meta({
             description:
               "An access token for the user in the UIUC Entra ID tenant.",
           }),
         }),

112-116: Fix misleading error message.

The error message says "Failed to get all member lists" but this endpoint retrieves RSVPs.

       if (!response || !response.Items) {
         throw new DatabaseFetchError({
-          message: "Failed to get all member lists.",
+          message: "Failed to fetch RSVPs for event.",
         });
       }

68-74: Set createdAt to an actual timestamp.

The createdAt field is set to an empty string, which defeats its purpose. This should be a proper ISO timestamp for auditing and sorting.

       const entry = {
         partitionKey: `${request.params.eventId}#${upn}`,
         eventId: request.params.eventId,
         userId: upn,
         isPaidMember,
-        createdAt: "",
+        createdAt: new Date().toISOString(),
       };
🧹 Nitpick comments (1)
src/api/routes/rsvp.ts (1)

52-52: Remove unused destructured variables.

givenName and surname are destructured but never used in the handler. Either remove them or use them (e.g., for logging or storing in the RSVP entry).

-      const { userPrincipalName: upn, givenName, surname } = verifiedData;
+      const { userPrincipalName: upn } = verifiedData;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 9e6ba13 and 96a29a1.

📒 Files selected for processing (2)
  • src/api/routes/rsvp.ts (1 hunks)
  • src/common/roles.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/api/routes/rsvp.ts (5)
src/api/components/index.ts (2)
  • withTags (243-258)
  • withRoles (194-241)
src/api/functions/uin.ts (1)
  • verifyUiucAccessToken (30-96)
src/common/errors/index.ts (2)
  • ValidationError (99-108)
  • DatabaseFetchError (121-130)
src/api/functions/membership.ts (1)
  • checkPaidMembership (359-400)
src/common/config.ts (1)
  • genericConfig (246-246)
🪛 ESLint
src/api/routes/rsvp.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/jZkjPRImBn'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 2-2: Unexpected use of file extension "js" for "api/plugins/rateLimiter.js"

(import/extensions)


[error] 3-3: Unexpected use of file extension "js" for "api/components/index.js"

(import/extensions)


[error] 11-11: Unexpected use of file extension "js" for "common/errors/index.js"

(import/extensions)


[error] 13-13: Unexpected use of file extension "js" for "api/functions/uin.js"

(import/extensions)


[error] 14-14: Unexpected use of file extension "js" for "api/functions/membership.js"

(import/extensions)


[error] 16-16: Unexpected use of file extension "js" for "common/config.js"

(import/extensions)


[error] 17-17: Unexpected use of file extension "js" for "common/roles.js"

(import/extensions)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Application
  • GitHub Check: Run Unit Tests
🔇 Additional comments (6)
src/common/roles.ts (2)

10-11: LGTM!

The new roles follow the existing naming convention and are properly added to the enum.


52-53: LGTM!

Human-readable mappings are correctly added, addressing the previous review comment about missing entries in AppRoleHumanMapper.

src/api/routes/rsvp.ts (4)

1-17: Imports look correct.

The .js extensions flagged by ESLint are a standard pattern for TypeScript projects using ES modules with Node.js resolution. The UnauthorizedError import, while not directly used in this file, is available for future use if needed.


19-24: Rate limiter configuration looks appropriate.

30 requests per 30 seconds is reasonable for RSVP operations.


83-100: Authorization properly implemented.

The GET endpoint correctly uses withRoles([AppRoles.VIEW_RSVPS], ...) and onRequest: fastify.authorizeFromSchema to restrict access.


117-122: Deduplication logic is correct.

Using Map to deduplicate by userId and spreading values() back into an array is an idiomatic approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants